#include <cserver/module.h>
#include <boost/property_tree/ptree.hpp>
#include <boost/property_tree/info_parser.hpp>
#include <boost/property_tree/json_parser.hpp>
#include <boost/lexical_cast.hpp>
#include <boost/log/trivial.hpp>
#include <sstream>
#include <cserver/restapi.h>
#include <cserver/uri.h>
#include <chrono>

namespace csrv{
	bool ModuleRegistry::registerModule(ModuleDef&& m){
		moduleDefs[m.name] = m;
		return true;
	}
	ModuleRegistry& ModuleRegistry::instance(){
		static ModuleRegistry the_one;
		return the_one;
	}

	ModuleRegisterHolder ModuleRegister::commit(){
		ModuleRegistry::instance().registerModule(std::move(module));
		return {};
	}
	ModuleRegister::ModuleRegister(const std::string& name){
		module.name = name;
	}
	ModuleRegister& ModuleRegister::operator()(const std::string& role){
		module.roles.insert(role);
		return *this;
	}
	RestApiTableType& GetRestApiTable(){
		static RestApiTableType the_one;
		return the_one;
	}
	RestApiAddHelper::RestApiAddHelper(std::string m, std::string c, RestApiHandler f, std::set<std::string> roles)
	{
		// TODO: check roles are declared in module definition
		GetRestApiTable()[m][c] = RestApiHanlderDef{f, roles};
	}
	SqlTableType& GetApiSqlTable(){
		static SqlTableType the_one;
		return the_one;
	}

	bool prepareInvocation(pqxx::prepare::invocation& invocation, std::ostream& fout, std::map<std::string, std::string>& query, const std::vector<std::pair<std::string, SqlArg>>& args, SessionInfo* user_info){
		auto& callback = query["callback"];
		for(auto& arg : args){
			auto pos = query.find(arg.first);
			if (pos == query.end()){
				if (arg.second.hasDefault){
					if (arg.second.defaultIsNull)
						invocation();
					else 
						invocation(arg.second.defaultValue);
				} 
				else if (user_info && arg.first == "__userid__")
					invocation(user_info->userid);
				else if (user_info && arg.first == "__orgid__")
					invocation(user_info->orgid);
				else{
					response_error(fout, 400, "ErrParameter", "MissingParameter " + arg.first, callback);
					return false;
				}
			}
			else {
				if (arg.first == "limit"){
					auto limit = boost::lexical_cast<unsigned>(pos->second);
					limit = std::min(limit, unsigned(2000));
					limit = std::max(limit, unsigned(1));
					invocation(limit);
				}
				else if (user_info && arg.first == "__userid__")
					invocation(user_info->userid);
				else if (user_info && arg.first == "__orgid__")
					invocation(user_info->orgid);
				else if (pos->second.empty())
					invocation();
				else
					invocation(pos->second);
			}
		}
		return true;
	}

	pqxx::result do_exec(pqxx::prepare::invocation& invo){
		auto result = invo.exec();
		BOOST_LOG_TRIVIAL(info) << "    " << result.query();
		return result;
	}

	template<typename Transaction>
	void callDB(Transaction& w, StreamGroup& sg, RequestContext& ctx, const std::string& callback, const SqlStatement& sql, std::map<std::string, std::string>& query, SessionInfo* user_info){
		std::stringstream  fout;
		if (!callback.empty()) fout << callback << "(";
		fout << R"({"func":")" << ctx.command << R"(","ret":{"success":true, "message":"",)";

		if (!sql.sql_total_name.empty()){
			auto invocation = w.prepared(sql.sql_total_name);
			if (!prepareInvocation(invocation, sg.fout, query, sql.argsTotal, user_info))
				return;

			fout << R"("total":)" << escape_null(do_exec(invocation).begin()->begin()->c_str()) << ",";
		}

		auto invocation = w.prepared(sql.sql_name);
		if (!prepareInvocation(invocation, sg.fout, query, sql.args, user_info))
			return;
		if (sql.type != "write")
			fout << R"("value":)" << escape_null(do_exec(invocation).begin()->begin()->c_str());
		else 
			fout << R"("code": )" << do_exec(invocation).affected_rows();

		fout << "}}";
		if (!callback.empty()) fout << ")";

		response_header(sg.fout, 200);
		sg.fout << fout.str();
	}

	GeneralSimpleApi::GeneralSimpleApi(const SqlStatement& s) : sql(s){}
	void GeneralSimpleApi::operator()(RequestContext& ctx){
		Request& req = ctx.req;
		auto query = parseQuery(req.getParam("QUERY_STRING"));
		auto& callback = query["callback"];
		SessionInfo* user_info = nullptr;
		SessionContext& sctx = ctx.sctx;
		auto pos = sctx.sessionTable.find(req.getCookie("sessionid"));
		if (pos != sctx.sessionTable.end()) user_info = &pos->second;

		StreamGroup sg(req, 4096);
		if (ctx.method != "GET" || sql.type != "read") {
			boost::property_tree::ptree ptree;
			read_json(sg.fin, ptree);
			for(auto& node : ptree){
				if (query.count(node.first)){
					response_error(sg.fout, 400, "ErrParameter", "DuplicatedParameter " + node.first, callback);
					return;
				}
				query[node.first] = node.second.data();
			}
			BOOST_LOG_TRIVIAL(info) << "    Read write transaction";
			pqxx::work w(ctx.conn);
			callDB(w, sg, ctx, callback, sql, query, user_info);
			w.commit();
		}
		else
		{
			BOOST_LOG_TRIVIAL(info) << "    Read only transaction";
			pqxx::read_transaction w(ctx.conn);
			callDB(w, sg, ctx, callback, sql, query, user_info);
		}
	}


	std::string parseSql(const std::string& origin, std::map<std::string, SqlArg>& args, std::vector<std::pair<std::string, SqlArg> >& out_args){
		auto start = origin.begin();
		auto end = origin.end();
		int field_id = 1;
		std::stringstream sstr;
		for(auto itr = std::find(start, end, '$'); itr != end; 
				itr = std::find(start, end, '$')){
			sstr << std::string(start, ++itr);

			start = std::find_if(itr, end, [](char ch){ return ch != '_' && !std::isalnum(ch);});

			std::string id(itr, start);
			if (id.empty())
				throw std::runtime_error("empty sql arg name");

			auto& arg = args[id];
			out_args.emplace_back(id, arg);

			sstr << field_id++; 
		}
		sstr << std::string(start, end);
		return sstr.str();
	}
	std::set<std::string> splitComma(const std::string& text, char delim = ','){
		std::set<std::string> result;
		auto end = text.end();
		for(auto itr = std::find_if(text.begin(), end, [](char ch){return !isspace(ch);}); itr != end; ){
			auto posc = std::find(itr, end, delim);
			auto endw = std::find_if(itr, posc, isspace);
			result.insert(std::string(itr, endw));

			if (posc != end) ++posc;
			itr = posc;
		}
		return result;
	}
	void loadSimpleApis(const std::string& path){
		boost::property_tree::ptree ptree;
		read_info(path, ptree);

		std::unordered_map<std::string, SqlStatement> all_sql;
		for(auto& node : ptree.get_child("sqls")){
			auto& st = all_sql[node.first];
			st.name = node.first;
			st.sql_name = st.name;
			st.origin = node.second.get<std::string>("statement");
			st.type = node.second.get<std::string>("type", "read");
			std::map<std::string, SqlArg> args;
			auto default_args = node.second.get_child_optional("default");
			if (default_args)
				for(auto& arg : *default_args){
					SqlArg sarg;
					auto value = arg.second.get_value_optional<std::string>();
					if (value)
						sarg.defaultValue = *value;
					else
						sarg.defaultIsNull = true;
					sarg.hasDefault = true;
					args[arg.first] = sarg;
				}
			bool is_collection = (node.second.find("iscollection") != node.second.not_found());
			if (is_collection) {
				st.sql_nocoll = parseSql(st.origin, args, st.nocoll_args);
				st.sql = parseSql(st.origin + "  limit $count offset $from", args, st.args);
			}
			else 
				st.sql = parseSql(st.origin, args, st.args);
		}

		std::set<std::string> default_roles{"admin", "user", "visitor"};
		for(auto& md : ptree.get_child("modules")){
			auto module_name = md.first;
			ModuleRegistry::instance().registerModule(ModuleDef{module_name, default_roles});
			for(auto& api : md.second){
				auto api_name = api.first;
				auto api_path = module_name + "/" + api_name;
				auto method = api.second.get<std::string>("method", "GET");
				auto sql = api.second.get<std::string>("sql");
				auto total_sql = api.second.get<std::string>("total_sql", "no");
				auto permit_roles = api.second.get<std::string>("permit_roles", "admin,user,visitor");

				auto sql_itr = all_sql.find(sql);
				if (sql_itr == all_sql.end())
					throw std::runtime_error("sql name not found");
				auto sql_statement = sql_itr->second;
				if (total_sql == "no"){
					sql_statement.totalSql.clear();
					sql_statement.argsTotal.clear();
					sql_statement.sql_total_name.clear();
				}
				else if (total_sql == "yes"){
					if (sql_statement.sql_nocoll.empty())
						throw std::runtime_error("Forget to specify 'iscollection' in sql " + sql + "?");
					sql_statement.totalSql = "select count(*) from (" + sql_statement.sql_nocoll + ") as __temp__";
					sql_statement.argsTotal = sql_statement.nocoll_args;
					sql_statement.sql_total_name = sql_statement.name + "__total_";
				}
				else {
					auto itr = all_sql.find(total_sql);
					if (itr == all_sql.end())
						throw std::runtime_error("sql name not found");
					sql_statement.totalSql = itr->second.sql;
					sql_statement.argsTotal = itr->second.args;
					sql_statement.sql_total_name = total_sql;
				}

				if (sql_statement.type == "read")
					sql_statement.sql = "select array_to_json(array_agg(row_to_json(__temp__))) from (" + sql_statement.sql + ") as __temp__";

				GetRestApiTable()[method][api_path] = RestApiHanlderDef{GeneralSimpleApi(sql_statement), splitComma(permit_roles)};
				GetApiSqlTable()[sql_statement.name] = sql_statement;
			}
		}

	}
}

